package dk.aiszone.lang.sql;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import dk.aiszone.lang.sql.interfaces.SQLChildHolder;
import dk.aiszone.lang.sql.interfaces.SQLConstraint;
import dk.aiszone.lang.sql.interfaces.SQLObject;
import dk.aiszone.lang.sql.interfaces.SQLPart;

/**
 * The <code>SQLResult</code> class contains the result of a &quot;prepared&quot; SQL statement.
 * 
 * @author Ais
 */
public class SQLResult {

    private StringBuilder currentBuilder;
    private List<SQLPart> finalParts = new ArrayList<SQLPart>();
    private Map<String, SQLAssignableField> missingAssignments = new HashMap<String, SQLAssignableField>();
    private boolean prepared = false;

    /**
     * Constructor for the <code>SQLResult</code> object.
     */
    /* pp */SQLResult() {
        finalParts.add(new SQLFinalPart(currentBuilder = new StringBuilder()));
    }

    /**
     * Append a <code>SQLConstraintHolder</code> to the <code>SQLResult</code>.
     * 
     * @param part
     *            The SQL part to add.
     * @return Conveniently returns <code>this</code>.
     */
    /* pp */SQLResult append(SQLChildHolder part) {
        finalParts.add(part);

        finalParts.add(new SQLFinalPart(currentBuilder = new StringBuilder()));
        return this;
    }

    /**
     * Append a <code>SQLConstraint</code> to the <code>SQLResult</code>.<br>
     * 
     * @param part
     *            The SQL part to add.
     * @return Conveniently returns <code>this</code>.
     */
    /* pp */SQLResult append(SQLConstraint part) {
        finalParts.add(part);

        finalParts.add(new SQLFinalPart(currentBuilder = new StringBuilder()));
        return this;
    }

    /**
     * Append a <code>SQLConstraintHolder</code> to the <code>SQLResult</code>.
     * 
     * @param part
     *            The SQL part to add.
     * @return Conveniently returns <code>this</code>.
     */
    /* pp */SQLResult append(SQLPart part) {
        finalParts.add(part);

        finalParts.add(new SQLFinalPart(currentBuilder = new StringBuilder()));
        return this;
    }

    /**
     * Append a <code>String</code> to the <code>SQLResult</code>
     * 
     * @param part
     *            The SQL part to add.
     * @return Conveniently returns <code>this</code>.
     */
    /* pp */SQLResult append(String part) {
        currentBuilder.append(part);
        return this;
    }

    /**
     * Append a <code>StringBuilder</code> to the <code>SQLResult</code>
     * 
     * @param part
     *            The SQL part to add.
     * @return Conveniently returns <code>this</code>.
     */
    /* pp */SQLResult append(StringBuilder part) {
        currentBuilder.append(part);
        return this;
    }

    /**
     * Set the value of the <code>SQLAssignable</code> object with the specified id.
     * 
     * @param id
     *            The Id of the <code>SQLAssignable</code> object to assign the value to.
     * @param value
     *            The value to assign to the <code>SQLAssignable</code> - can be <code>null</code>.
     * @throws UnknownIdException
     *             Thrown if the specified id is unknown.
     */
    /* pp */void setValue(String id, Object value) throws UnknownIdException {
        if (!prepared) {
            prepare();
        }

        SQLAssignableField field = missingAssignments.get(id);
        if (field == null) {
            throw new UnknownIdException(id);
        }
        field.setValue(value);
    }

    /**
     * Convert the <code>SQLResult</code> to a prepared statement <code>String</code>.
     * 
     * @return Prepared statement syntax <code>String</code> representation of the <code>SQLResult</code> object.
     */
    /* pp */String toPreparedStatement() {
        prepare();

        // TODO: Calculate a good buffer width + and add 20 %
        StringBuilder sql = new StringBuilder(100);

        for (Iterator<SQLPart> partIterator = finalParts.iterator(); partIterator.hasNext();) {
            sql.append(partIterator.next().toPreparedStatement());
        }

        return sql.toString();
    }

    /**
     * Convert the <code>SQLResult</code> to a <code>String</code>.
     * 
     * @return <code>String</code> representation of the <code>SQLResult</code> object.
     */
    /* pp */String toSQL() {
        prepare();

        // TODO: Calculate a good buffer width + and add 20 %
        StringBuilder sql = new StringBuilder(100);

        for (Iterator<SQLPart> partIterator = finalParts.iterator(); partIterator.hasNext();) {
            sql.append(partIterator.next().toSQL());
        }

        return sql.toString();
    }

    private void handlePart(SQLObject part) {
        if (part instanceof SQLChildHolder) {
            SQLChildHolder constraintHolder = (SQLChildHolder)part;
            for (int i = 0; i < constraintHolder.getChildCount(); i++) {
                handlePart(constraintHolder.getChild(i));
            }
        } else if (part instanceof SQLConstraint) {
            SQLAssignableField assignable = ((SQLConstraint)part).getAssignableField();
            if (!assignable.isAssigned()) {
                missingAssignments.put(assignable.getId(), assignable);
            }
        }
    }

    /**
     * Causes the <code>SQLReult</code> to convert to a prepared result.<br>
     * The prepared result
     */
    private void prepare() {
        if (this.prepared) {
            return;
        }

        this.prepared = true;
        for (Iterator<SQLPart> partIterator = finalParts.iterator(); partIterator.hasNext();) {
            SQLPart part = partIterator.next();

            if (part instanceof SQLObject) {
                handlePart((SQLObject)part);
            }
        }
    }

    /**
     * Thrown if the specified id is unknown.
     */
    public static class UnknownIdException extends Exception {

        /* pp */UnknownIdException(String id) {
            super("Unknown id: " + id);
        }
    }

    private class SQLFinalPart implements SQLPart {

        /* pp */final StringBuilder part;

        /* pp */SQLFinalPart(StringBuilder part) {
            this.part = part;
        }

        @Override
        public String toPreparedStatement() {
            return toSQL();
        }

        @Override
        public String toSQL() {
            return part.toString();
        }

        @Override
        public String toString() {
            return super.toString() + "[" + part.toString() + "]";
        }
    }
}